【C++】weak 您所在的位置:网站首页 c 引用 指针 【C++】weak

【C++】weak

2023-07-21 04:50| 来源: 网络整理| 查看: 265

weak_ptr这个指针天生一副小弟的模样,也是在C++11的时候引入的标准库,它的出现完全是为了弥补它老大shared_ptr天生有缺陷的问题。

相比于上一代的智能指针auto_ptr来说,新进老大shared_ptr可以说近乎完美,但是通过引用计数实现的它,虽然解决了指针独占的问题,但也引来了引用成环的问题,这种问题靠它自己是没办法解决的,所以在C++11的时候将shared_ptr和weak_ptr一起引入了标准库,用来解决循环引用的问题。

循环引用

什么是循环引用的问题呢?在shared_ptr的使用过程中,当强引用计数为0是,就会释放所指向的堆内存。那么问题来了,如果和死锁一样,当两个shared_ptr互相引用,那么它们就永远无法被释放了。

例如:

#include #include class CB; class CA { public: CA() { std::cout m_ptr_b = ptr; } private: std::shared_ptr m_ptr_b; }; class CB { public: CB() { std::cout m_ptr_a = ptr; } private: std::shared_ptr m_ptr_a; }; int main() { std::shared_ptr ptr_a(new CA()); std::shared_ptr ptr_b(new CB()); ptr_a->set_ptr(ptr_b); ptr_b->set_ptr(ptr_a); std::cout std::cout m_ptr_a = ptr; } private: std::weak_ptr m_ptr_a; };

编译并运行结果,打印为:

yngzmiao@yngzmiao-virtual-machine:~/test$ ./main CA() CB() 1 2 ~CA() ~CB()

通过这次结果可以看到,CA和CB的对象都被正常的析构了。修改后例子中的引用关系如下图所示:

流程与上一例子大体相似,但是不同的是④这条引用是通过weak_ptr建立的,并不会增加引用计数。也就是说,CA的对象只有一个引用计数,而CB的对象只有2个引用计数,当main函数返回时,对象ptr_a和ptr_b被销毁,也就是①、③两条引用会被断开,此时CA对象的引用计数会减为0,对象被销毁,其内部的m_ptr_b成员变量也会被析构,导致CB对象的引用计数会减为0,对象被销毁,进而解决了引用成环的问题。

如果仔细看代码的话,会觉得很神奇!定义m_ptr_a修改成std::weak_ptr类型,但是set_ptr函数定义的参数还是std::shared_ptr类型。这个时候为什么没有报错?weak_ptr和shared_ptr的联系是什么呢?

weak_ptr的原理

weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。从这个角度看,weak_ptr更像是shared_ptr的一个助手而不是智能指针。

初始化方式 通过shared_ptr直接初始化,也可以通过隐式转换来构造;允许移动构造,也允许拷贝构造。 #include #include class Frame {}; int main() { std::shared_ptr f(new Frame()); std::weak_ptr f1(f); // shared_ptr直接构造 std::weak_ptr f2 = f; // 隐式转换 std::weak_ptr f3(f1); // 拷贝构造函数 std::weak_ptr f4 = f1; // 拷贝构造函数 std::weak_ptr f5; f5 = f; // 拷贝赋值函数 f5 = f2; // 拷贝赋值函数 std::cout } void showID() { std::cout Test* t = new Test(2); std::thread t1(thread1, t); delete t; t1.join(); return 0; }

在例子中,由于thread1等待2s,此时,main线程早已经把t对象析构了。打印m_id,自然不能打印出2了。可以通过shared_ptr和weak_ptr来解决共享对象的线程安全问题。

#include #include #include class Test { public: Test(int id) : m_id(id) {} void showID() { std::cout std::shared_ptr sp = std::make_shared(2); std::thread t2(thread2, sp); t2.join(); return 0; }

如果想访问对象的方法,先通过t的lock方法进行提升操作,把weak_ptr提升为shared_ptr强智能指针。提升过程中,是通过检测它所观察的强智能指针保存的Test对象的引用计数,来判定Test对象是否存活。ps如果为nullptr,说明Test对象已经析构,不能再访问;如果ps!=nullptr,则可以正常访问Test对象的方法。

如果设置t2为分离线程t2.detach(),让main主线程结束,sp智能指针析构,进而把Test对象析构,此时showID方法已经不会被调用,因为在thread2方法中,t提升到sp时,lock方法判定Test对象已经析构,提升失败!

观察者模式

观察者模式就是,当观察者观察到某事件发生时,需要通知监听者进行事件处理的一种设计模式。

在多数实现中,观察者通常都在另一个独立的线程中,这就涉及到在多线程环境中,共享对象的线程安全问题(解决方法就是使用上文的智能指针)。这是因为在找到监听者并让它处理事件时,其实在多线程环境中,肯定不明确此时监听者对象是否还存活,或是已经在其它线程中被析构了,此时再去通知这样的监听者,肯定是有问题的。

也就是说,当观察者运行在独立的线程中时,在通知监听者处理该事件时,应该先判断监听者对象是否存活,如果监听者对象已经析构,那么不用通知,并且需要从map表中删除这样的监听者对象。其中的主要代码为:

// 存储监听者注册的感兴趣的事件 unordered_map listenerMap; // 观察者观察到事件发生,转发到对该事件感兴趣的监听者 void dispatchMessage(int msgid) { auto it = listenerMap.find(msgid); if (it != listenerMap.end()) { for (auto it1 = it->second.begin(); it1 != it->second.end(); ++it1) { shared_ptr ps = it1->lock(); // 智能指针的提升操作,用来判断监听者对象是否存活 if (ps != nullptr) { // 监听者对象如果存活,才通知处理事件 ps->handleMessage(msgid); } else { it1 = it->second.erase(it1); // 监听者对象已经析构,从map中删除这样的监听者对象 } } } }

这个想法来源于:一个用C++写的开源网络库,muduo库,作者陈硕。大家可以在网上下载到muduo的源代码,该源码中对于智能指针的应用非常优秀,其中借助shared_ptr和weak_ptr解决了这样一个问题,多线程访问共享对象的线程安全问题。

解决循环引用

循环引用,简单来说就是:两个对象互相使用一个shared_ptr成员变量指向对方的会造成循环引用,导致引用计数失效。上文详细讲述了循环引用的错误原因和解决办法。

监视this智能指针

在上文讲述shared_ptr的博文中就有讲述到:enable_shared_from_this中有一个弱指针weak_ptr,这个弱指针能够监视this。在调用shared_from_this这个函数时,这个函数内部实际上是调用weak_ptr的lock方法。lock()会让shared_ptr指针计数+1,同时返回这个shared_ptr。

相关阅读 C++设计模式 - 观察者Observer模式第22课 weak_ptr弱引用智能指针智能指针(三):weak_ptr浅析深入掌握C++智能指针


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有